/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using VisualEsse.DataEngine;
using System.IO.Compression;

namespace VisualEsse.THREDDS
{
    public class ThreddsRequest
    {
        private string url;
        private List<ThreddsRequestVariable> variables;

        private bool gzipComressionOn = false;

        public ThreddsRequest(string url)
        {
            this.url = url;
            variables = new List<ThreddsRequestVariable>(6);
        }

        public IList<ThreddsVariable> GetMetadata()
        {
            List<ThreddsVariable> vars;

            if (url.IndexOf("http://") != -1)
            {
                string requestString = url + ".dds";

                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestString);
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                using (Stream stream = response.GetResponseStream())
                {
                    vars = ThreddsResponse.ParseDDS(stream);
                }

                return vars;
            }
            else
            {
                // Getting metadata from the local file
                using (FileStream fs = new FileStream(url, FileMode.Open, FileAccess.Read))
                {
                    if (url.EndsWith(".gzip") || url.EndsWith(".gz")) // Add additional decompressing stream if needed
                        vars = ThreddsResponse.ParseDDS(new GZipStream(fs, CompressionMode.Decompress));
                    else
                        vars = ThreddsResponse.ParseDDS(fs);
                }

                return vars;
            }
        }

        public bool ContentGZipped
        {
            get { return gzipComressionOn; }
            set { gzipComressionOn = value; }
        }

        public void AddVariable(string name, Type type, int min, int max, int stride)
        {
            variables.Add(new ThreddsRequestVariable(name, type, min, max, stride));
        }

        public void AddVariable(string name, Type type, int min1, int max1, int stride1, int min2, int max2, int stride2)
        {
            variables.Add(new ThreddsRequestVariable(name, type, min1, max1, stride1, min2, max2, stride2));
        }

        private string GetRequestURL()
        {
            StringBuilder requestStringBuilder = new StringBuilder(url);
            // TODO: for binary: requestStringBuilder = requestStringBuilder.Append(".dods?");
            //requestStringBuilder = requestStringBuilder.Append(".ascii?");
            requestStringBuilder = requestStringBuilder.Append(".dods?");

            for (int i = 0; i < variables.Count; i++)
            {
                ThreddsRequestVariable v = variables[i];
                if (i > 0)
                    requestStringBuilder = requestStringBuilder.Append(',');
                requestStringBuilder = requestStringBuilder.AppendFormat("{0}[{1}:{2}:{3}]",                
                    v.name, v.min, v.stride, v.max);

                if(v.IsTwoDimensional)
                    requestStringBuilder = requestStringBuilder.AppendFormat("[{0}:{1}:{2}]",
                    v.min2, v.stride2, v.max2);
                
            }
            return requestStringBuilder.ToString();
        }

        private HttpWebResponse GetWebResponse()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(GetRequestURL());
            return (HttpWebResponse)request.GetResponse();
        }

        public void StoreText(string path)
        {
            StreamWriter output = new StreamWriter(path);
            StreamReader input = new StreamReader(GetResponseStream());
            while (!input.EndOfStream)
            {
                output.WriteLine(input.ReadLine());
            }
            output.Close();
        }

        public Stream GetResponseStream()
        {
            if (url.IndexOf("http://") == -1)
            {
                // Working with local file
                if (!File.Exists(url))
                    throw new Exception("THREDDS request: Couldn't find requested local file with data");
                string ext = Path.GetExtension(url);
                if (ext == ".gzip" || ext == ".gz")
                    ContentGZipped = true;

                Stream stream = new FileStream(url, FileMode.Open);
                if (ContentGZipped)
                    stream = new System.IO.Compression.GZipStream(stream, System.IO.Compression.CompressionMode.Decompress);
                return stream;
            }
            
            // Working via Web
            HttpWebResponse webResponse = GetWebResponse();
            return webResponse.GetResponseStream();
        }

        public ThreddsResponse GetResponse()
        {
            ThreddsResponseText threddsResponse;
            using (Stream stream = GetResponseStream())
            {
                threddsResponse = new ThreddsResponseText(stream, variables);
            }

            return threddsResponse;
        }

        public ThreddsResponseBinary GetBinaryResponse()
        {
            ThreddsResponseBinary threddsResponse;
            using (Stream stream = GetResponseStream())
            {
                threddsResponse = new ThreddsResponseBinary(stream, variables);
            }

            return threddsResponse;
        }

        internal struct ThreddsRequestVariable
        {
            public string name;
            public int min;
            public int max;
            public int stride;
            public Type type;

            public int min2;
            public int max2;
            public int stride2;

            public ThreddsRequestVariable(string name, Type type, int min, int max, int stride)
            {
                this.type = type;
                this.name = name;
                this.min = min;
                this.max = max;
                this.stride = stride;
                this.min2 = this.stride2 = this.max2 = -1;
            }

            public ThreddsRequestVariable(string name, Type type, int min, int max, int stride, int min2, int max2, int stride2)
            {
                this.type = type;
                this.name = name;
                this.min = min;
                this.max = max;
                this.stride = stride;

                this.min2 = min2;
                this.max2 = max2;
                this.stride2 = stride2;
            }

            public bool IsTwoDimensional
            {
                get
                {
                    return min2 != -1;
                }
            }
        }
    }
 
    public abstract class ThreddsResponse
    {
        protected ThreddsVariable[] variables;

        internal ThreddsResponse(int count)
        {
            variables = new ThreddsVariable[count];
        }

        public ThreddsVariable[] Variables
        {
            get
            {
                return variables;
            }
        }

        public ThreddsVariable this[string variableName]
        {
            get
            {
                foreach (ThreddsVariable v in variables)
                {
                    if (v.Name == variableName)
                        return v;
                }

                throw new Exception("Variable not found");
            }
        }

        public static List<ThreddsVariable> ParseDDS(Stream stream)
        {
            List<ThreddsVariable> result = new List<ThreddsVariable>();
            Parser parser = new Parser(stream);
            if (parser.GetToken() != TokenType.Ident || parser.TokenText != "Dataset")
                throw new Exception("Wrong DDS declaration - no Dataset token");
            if (parser.GetToken() != TokenType.Char || parser.TokenText != "{")
                throw new Exception("Wrong DDS declaration - syntax error!");
            while (parser.GetToken() == TokenType.Ident)
            {
                Type type = GetTypeFromString(parser.TokenText);
                if(parser.GetToken() != TokenType.Ident)
                    throw new Exception("Wrong DDS declaration - name expected");
                string name = parser.TokenText;
                List<int> dimensions = new List<int>();
                while (parser.GetToken() == TokenType.Char && parser.TokenText == "[")
                {
                    if (parser.GetToken() != TokenType.Ident)
                        throw new Exception("Wrong DDS declaration - no dimension name");
                    if (parser.GetToken() != TokenType.Char || parser.TokenText != "=")
                        throw new Exception("Wrong DDS declaration - no equal sign");
                    if (parser.GetToken() != TokenType.Integer)
                        throw new Exception("Wrong DDS declaration - no dimension size");
                    dimensions.Add(parser.IntegerValue);
                    if (parser.GetToken() != TokenType.Char || parser.TokenText != "]")
                        throw new Exception("Wrong DDS declaration - no closing bracket");
                }
                if(parser.TokenType != TokenType.Char || parser.TokenText!= ";")
                    throw new Exception("Wrong DDS declaration - syntax error!");
                if (dimensions.Count == 0)
                    throw new Exception("Wrong DDS declaration - scalar values are not supported");
                int[] min = new int[dimensions.Count];
                int[] max = new int[dimensions.Count];
                int[] stride = new int[dimensions.Count];
                int idx = 0;
                foreach (int d in dimensions)
                {
                    min[idx] = 0;
                    max[idx] = d - 1;
                    stride[idx++] = 1;
                }
                result.Add(new ThreddsVariable(name, type, min, max, stride, null));
            }
            if(parser.TokenType != TokenType.Char || parser.TokenText!= "}")
                throw new Exception("Wrong DDS declaration - syntax error!");
            return result;
        }

        private static Type GetTypeFromString(string text)
        {
            switch (text)
            {
                case "Int32":
                    return typeof(int);
                case "Float32":
                    return typeof(float);
                case "Float64":
                    return typeof(double);
                case "Byte":
                    return typeof(byte);
                default:
                    throw new Exception("Wrong DDS declaration - unknown type " + text);
            }
        }
    }

    internal class ThreddsResponseText : ThreddsResponse
    {
        internal ThreddsResponseText(Stream responseStream, List<ThreddsRequest.ThreddsRequestVariable> variables)
            : base(variables.Count)
        {
            ParseStream(responseStream, variables);
        }

        private void ParseStream(Stream responseStream, List<ThreddsRequest.ThreddsRequestVariable> variables)
        {
            ThreddsRequest.ThreddsRequestVariable v;

            // Parsing response stream
            using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
            {
                while (reader.ReadLine() != "---------------------------------------------") ;

                string line, name;
                int startIdx;
                for (int i = 0; i < variables.Count; i++)
                {
                    do
                    {
                        // Skipping empty lines
                        do
                        {
                            if (reader.EndOfStream)
                                throw new Exception("THREDDS Response: some of requested variables not found");

                            line = reader.ReadLine();
                        } while (line.Trim() == string.Empty);

                        // Reading the name of the variable
                        startIdx = line.IndexOf('[');
                        name = line.Substring(0, startIdx); // name of the variable                    

                        try
                        {
                            // Trying to find the related variable
                            v = FindRequestedVariable(variables, name);
                            break;
                        }
                        catch
                        {
                            // Not requested variable found; skip it.
                            do
                            {
                                if (reader.EndOfStream)
                                    throw new Exception("THREDDS Response: some of requested variables not found");

                                line = reader.ReadLine();
                            } while (line != string.Empty);
                            continue; // Continue out searchings
                        }
                    } while (true); // Searching for the next variable

                    // Reading data for the variable
                    if (v.IsTwoDimensional)
                    {
                        int endIdx = line.IndexOf(']');
                        int count1 = Int32.Parse(line.Substring(startIdx + 1, endIdx - startIdx - 1));

                        int endIdx2 = line.IndexOf(']', endIdx + 1);
                        int count2 = Int32.Parse(line.Substring(endIdx + 2, endIdx2 - endIdx - 2));
                        this.variables[i] = ReadVariable(reader, v, count1, count2);
                    }
                    else
                    {
                        // Reading one-dimensional variable
                        int endIdx = line.IndexOf(']');
                        int count1 = Int32.Parse(line.Substring(startIdx + 1, endIdx - startIdx - 1));
                        this.variables[i] = ReadVariable(reader, v, count1);
                    }
                }
            }
        }

        private static ThreddsRequest.ThreddsRequestVariable FindRequestedVariable(List<ThreddsRequest.ThreddsRequestVariable> list, string name)
        {
            foreach (ThreddsRequest.ThreddsRequestVariable var in list)
            {
                if (var.name == name)
                    return var;
            }

            // This variable must be skippid since it was not requested! 
            throw new Exception("Not requested variable found");
        }

        private ThreddsVariable ReadVariable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count)
        {
            if (inVar.type == typeof(Int32))
                return ReadInt32Variable(r, inVar, count);
            if (inVar.type == typeof(Double))
                return ReadReal64Variable(r, inVar, count);
            if (inVar.type == typeof(Single))
                return ReadReal32Variable(r, inVar, count);
            if (inVar.type == typeof(Byte))
                return ReadInt8Variable(r, inVar, count);

            throw new Exception("THREDDS Response: used variable type is not supported (" + inVar.type.ToString() + ")");
        }

        private ThreddsVariable ReadVariable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count1, int count2)
        {            
            if (inVar.type == typeof(Byte))
                return ReadInt8Variable2(r, inVar, count1, count2);

            throw new Exception("THREDDS Response: used variable type is not supported (" + inVar.type.ToString() + ")");
        }

        private ThreddsVariable ReadInt8Variable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count)
        {            
            byte[] data = new byte[count];
            string s = r.ReadLine();
            string[] items = s.Split(',');

            for (int i = 0; i < count; i++)
            {
                data[i] = byte.Parse(items[i]);
            }

            return new ThreddsVariable(inVar.name, inVar.type, inVar.min, inVar.max, inVar.stride, data);
        }

        private ThreddsVariable ReadInt8Variable2(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count1, int count2)
        {            
            byte[,] data = new byte[count1, count2];

            for (int line = 0; line < count1; line++)
            {
                string s = r.ReadLine();
                string[] items = s.Split(',');

                for (int i = 0; i < count2; i++)
                {
                    data[line, i] = byte.Parse(items[i + 1]);
                }
            }

            return new ThreddsVariable(inVar.name, inVar.type, inVar.min, inVar.max, inVar.stride,
                inVar.min2, inVar.max2, inVar.stride2, data);
        }

        private ThreddsVariable ReadReal32Variable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count)
        {
            Single[] data = new Single[count];
            string s = r.ReadLine();
            string[] items = s.Split(',');

            for (int i = 0; i < count; i++)
            {
                data[i] = Single.Parse(items[i],System.Globalization.CultureInfo.InvariantCulture);
            }

            return new ThreddsVariable(inVar.name, inVar.type, inVar.min, inVar.max, inVar.stride, data);
        }

        private ThreddsVariable ReadReal64Variable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count)
        {
            double[] data = new double[count];
            string s = r.ReadLine();
            string[] items = s.Split(',');

            for (int i = 0; i < count; i++)
            {
                data[i] = double.Parse(items[i],System.Globalization.CultureInfo.InvariantCulture);
            }

            return new ThreddsVariable(inVar.name, inVar.type, inVar.min, inVar.max, inVar.stride, data);
        }

        private ThreddsVariable ReadInt32Variable(StreamReader r, ThreddsRequest.ThreddsRequestVariable inVar, int count)
        {            
            Int32[] data = new Int32[count];
            string s = r.ReadLine();
            string[] items = s.Split(',');

            for (int i = 0; i < count; i++)
            {
                data[i] = Int32.Parse(items[i]);
            }

            return new ThreddsVariable(inVar.name, inVar.type, inVar.min, inVar.max, inVar.stride, data);
        }
    }

    public class ThreddsResponseBinary : ThreddsResponse
    {
        internal ThreddsResponseBinary(Stream responseStream, List<ThreddsRequest.ThreddsRequestVariable> variables)
            : base(variables.Count)
        {
            string temp = null;
            try
            {
                // TODO : remove
                int read, total = 0;
                temp = Path.GetTempFileName();
                using (Stream output = new FileStream(temp, FileMode.Create, FileAccess.ReadWrite))
                {
                    using (Stream input = responseStream)
                    {
                        byte[] buffer = new byte[100 * 1024];
                        do
                        {
                            read = input.Read(buffer, 0, buffer.Length);
                            output.Write(buffer, 0, read);
                            total += read;
                        } while (read != 0);
                    }

                    output.Seek(0, SeekOrigin.Begin);
                    responseStream = output;

                    // Get variables in response
                    List<ThreddsVariable> variablesList = ThreddsResponse.ParseDDS(responseStream);
                    this.variables = (ThreddsVariable[])variablesList.ToArray();
                    // Parsing response stream
                    using (BinaryReader reader = new BinaryReader(responseStream, Encoding.UTF8))
                    {
                        while (ReadLine(reader) != "Data:") ;

                        for (int i = 0; i < this.variables.Length; i++)
                        {
                            ReadVariable(reader, this.variables[i]);
                        }
                    }
                }
            }
            finally
            {
                if (temp != null)
                    File.Delete(temp);
            }
        }

        private void ReadVariable(BinaryReader r, ThreddsVariable inVar)
        {
            if (inVar.Type == typeof(Int32))
                ReadInt32Variable(r, inVar);
            else if (inVar.Type == typeof(Double))
                ReadReal64Variable(r, inVar);
            else if (inVar.Type == typeof(Single))
                ReadReal32Variable(r, inVar);
            else if (inVar.Type == typeof(Byte))
                ReadInt8Variable(r, inVar);
            else
                throw new Exception("THREDDS Response: used variable type is not supported (" + inVar.Type.ToString() + ")");
        }

        /// <summary>Reads array of ints from BinaryReaders. Performs byte order conversion</summary>
        /// <param name="r">BinaryReader to read from</param>
        /// <param name="dst">Array to write to</param>
        private void ReadArray(BinaryReader r,int[] dst)
        {
            byte[] src = r.ReadBytes(dst.Length * 4); 
            unsafe
            {
                fixed(int *d = &dst[0])
                fixed(byte* s = &src[0])
                {
                    byte * d2 = (byte*)d;
                    byte* s2 = s;
                    for (int i = 0; i < dst.Length; i++)
                    {
                        d2[3] = s2[0];
                        d2[2] = s2[1];
                        d2[1] = s2[2];
                        d2[0] = s2[3];
                        d2 += 4;
                        s2 += 4;
                    }
                }
            }
        }

        /// <summary>Reads array of doubles from BinaryReaders. Performs byte order conversion</summary>
        /// <param name="r">BinaryReader to read from</param>
        /// <param name="dst">Array to write to</param>
        private void ReadArray(BinaryReader r, double[] dst)
        {
            byte[] src = r.ReadBytes(dst.Length * 8);
            unsafe
            {
                fixed (double* d = &dst[0])
                fixed (byte* s = &src[0])
                {
                    byte* d2 = (byte*)d;
                    byte* s2 = s;
                    for (int i = 0; i < dst.Length; i++)
                    {
                        d2[7] = s2[0];
                        d2[6] = s2[1];
                        d2[5] = s2[2];
                        d2[4] = s2[3];
                        d2[3] = s2[4];
                        d2[2] = s2[5];
                        d2[1] = s2[6];
                        d2[0] = s2[7];
                        d2 += 8;
                        s2 += 8;
                    }
                }
            }
        }

        /// <summary>Reads array of floats from BinaryReaders. Performs byte order conversion</summary>
        /// <param name="r">BinaryReader to read from</param>
        /// <param name="dst">Array to write to</param>
        private void ReadArray(BinaryReader r, float[] dst)
        {
            byte[] src = r.ReadBytes(dst.Length * 4);
            unsafe
            {
                fixed (float* d = &dst[0])
                fixed (byte* s = &src[0])
                {
                    byte* d2 = (byte*)d;
                    byte* s2 = s;
                    for (int i = 0; i < dst.Length; i++)
                    {
                        d2[3] = s2[0];
                        d2[2] = s2[1];
                        d2[1] = s2[2];
                        d2[0] = s2[3];
                        d2 += 4;
                        s2 += 4;
                    }
                }
            }
        }

        /// <summary>Reads two dimensional array of bytes from binary reader</summary>
        /// <param name="r">BinaryReader to read from</param>
        /// <param name="dst">Array to store to</param>
        private void ReadArray(BinaryReader r, byte[,] dst)
        {
            int dim0 = dst.GetLength(0);
            int dim1 = dst.GetLength(1);
            byte[] buffer = new byte[dim1];
            for (int i = 0; i < dim0; i++)
            {
                int count = 0;
                do
                {
                    count += r.Read(buffer, count, dim1 - count);
                } while (count < dim1);
                for (int j = 0; j < dim1; j++)
                    dst[i, j] = buffer[j];
            }
        }

        private void CheckDimensions(BinaryReader r, ThreddsVariable v)
        {
            int[] dim = new int[2];
            ReadArray(r, dim);
            //if (dim[0] != dim[1])
            //    throw new Exception("Error reading DODS format - dimensions are not duplicated for variable " + v.Name);
            //if (dim[0] != v.Length)
            //    throw new Exception("Error reading DODS format - dimensions do not match for variable " + v.Name);
        }

        private void ReadInt8Variable(BinaryReader r, ThreddsVariable var)
        {
            CheckDimensions(r, var);
            if (var.Dimensions == 1)
                var.Data = r.ReadBytes(var.Length);
            else if (var.Dimensions == 2)
            {
                byte[,] data = new byte[var.GetMax(0) + 1, var.GetMax(1) + 1];
                ReadArray(r, data);
                var.Data = data;
            }
            else
                throw new Exception("Error readings DODS format - more than two dimensions is not supported");
            int padding = 4 - var.Length % 4;
            if (padding != 4)
                r.ReadBytes(padding);
        }

        private void ReadReal32Variable(BinaryReader r, ThreddsVariable var)
        {
            CheckDimensions(r, var);
            int count = var.GetMax(0) + 1;
            Single[] data = new Single[count];
            ReadArray(r, data);
            var.Data = data;
        }

        private void ReadReal64Variable(BinaryReader r, ThreddsVariable var)
        {
            CheckDimensions(r, var);
            int count = var.GetMax(0) + 1;
            double[] data = new double[count];
            ReadArray(r, data);
            var.Data = data;
        }

        private void ReadInt32Variable(BinaryReader r, ThreddsVariable var)
        {
            CheckDimensions(r, var);
            int count = var.GetMax(0) + 1;
            Int32[] data = new Int32[count];
            ReadArray(r,data);
            var.Data = data;
        }

        private string ReadLine(BinaryReader reader)
        {
            StringBuilder sb = new StringBuilder();
            char ch;
            while ((ch = reader.ReadChar()) != '\n')
            {
                sb = sb.Append(ch);
            }

            return sb.ToString();
        }        
    }
}